[Previous] [Next]

The Windows Registry

The Windows Registry is the area where the operating system and most applications store their configuration values. You must be able to read as well as to write data into the Registry, in order to build flexible applications that adapt themselves to their environment.

Visual Basic Built-In Functions

Unfortunately, the support for the Registry offered by Visual Basic leaves much to be desired and is limited to the following four commands and functions:

' Save a value.
SaveSetting AppName, Section, Key, Setting
' Read a value. (The Default argument is optional.)
value = GetSetting(AppName, Section, Key, Default)
' Return a list of settings and their values.
values = GetAllSettings(AppName, Section)
' Delete a value. (Section and Key arguments are optional.)
DeleteSetting AppName, Section, Key

These four commands can't read and write to any area in the Registry but are limited to the HKEY_CURRENT_USER\Software\VB and VBA Program Settings subtree of the Registry. For example, you can use the SaveSetting function to store the initial function position and size of the main form in the MyInvoicePrg application:

SaveSetting "MyInvoicePrg", "frmMain", "Left", frmMain.Left
SaveSetting "MyInvoicePrg", "frmMain", "Top", frmMain.Top
SaveSetting "MyInvoicePrg", "frmMain", "Width", frmMain.Width
SaveSetting "MyInvoicePrg", "frmMain", "Height", frmMain.Height

You can see the result of this sequence of statements in Figure A-5.

Click to view at full size.

Figure A-5. All Visual Basic Registry functions read and write values in the HKEY_CURRENT_USER\Software\VB and VBA Program Settings subtree.

You can then read back these settings using the GetSetting function:

' Use the Move method to avoid multiple Resize and Paint events.
frmMain.Move GetSetting("MyInvoicePrg", "frmMain", "Left", "1000"), _
    GetSetting("MyInvoicePrg", "frmMain", "Top", "800"), _
    GetSetting("MyInvoicePrg", "frmMain", "Width", "5000"), _
    GetSetting("MyInvoicePrg", "frmMain", "Height", "4000")

If the specified key doesn't exist, the GetSetting function either returns the values passed to the Default argument, or it returns an empty string if that argument is omitted. GetAllSettings returns a two dimensional array, which contains all the keys and the values under a given section:

Dim values As Variant, i As Long
values = GetAllSettings("MyInvoicePrg", "frmMain")
' Each row holds two items, the key name and the key value.
For i = 0 To UBound(settings)
    Print "Key =" & values(i, 0) & "  Value = " & values(i, 1)
Next

The last keyword of the group, DeleteSetting, can delete an individual key, or it can delete all the keys under a given section if you omit its last argument:

' Delete the "Left" key for the frmMain form.
DeleteSetting "MyInvoicePrg", "frmMain", "Left"
' Delete all the settings for the frmMain form.
DeleteSetting "MyInvoicePrg", "frmMain"

The demonstration program shown in Figure A-6 demonstrates how you can use the Visual Basic's built-in Registry functions to save and to restore form settings.

Figure A-6. The demonstration program contains reusable routines for saving and restoring form settings to the Registry.

The API Functions

While the Visual Basic built-in functions are barely versatile enough for saving and restoring program configuration values, they entirely lack the functionality for accessing any region of the Registry, which you must have in order to read some important settings of the operating system. Luckily, the Windows API contains all the functions you need to perform this task.

WARNING
You must be very careful when you play with the Registry in this way because you might corrupt the installation of other applications or the operating system itself, and you might even be force to reinstall them. but in general, you can't do much harm if you simply read values in the Registry and don't write to it. To reduce risks, however, you might want to back up your system Registry so that you have a copy to restore if something goes wrong.

Predefined keys

Before starting to play with API functions, you must have a broad idea of how the Registry is arranged. The system Registry is a hierarchical structure that consists of keys, subkeys, and values. More precisely, the Registry has a number of predefined top-level keys, which I've summarized in Table A-1.

Table A-1. The predefined Registry keys.

Key Value Description
HKEY_CLASSES_ROOT &H80000000 The subtree that contains all the information about COM components installed on the machine. (It's actually a subtree of the HKEY_LOCAL_MACHINE key but also appears as a top-level key.)
HKEY_CURRENT_USER &H80000001 The subtree that contains the preferences for the current user. (It's actually to a subtree of the HKEY_USERS key but also appears as a top-level key.)
HKEY_LOCAL_MACHINE &H80000002 The subtree that contains information about the physical configuration of the computer, including installed hardware and software.
HKEY_USERS &H80000003 The subtree that contains the default user configuration and also contains information about the current user.
HKEY_PERFORMANCE_DATA &H80000004 The subtree that collects performance data; data is actually stored outside the Registry, but appears to be part of it. (It's available only in Windows NT.)
HKEY_CURRENT_CONFIG &H80000005 The subtree that contains data about the current configuration. (It corresponds to a subtree of the HKEY_LOCAL_MACHINE key but also appears as a top-level key.)
HKEY_DYN_DATA &H80000006 The subtree that collects performance data; this portion of the Registry is reinitialized at each reboot. (It's available only in Windows 95 and 98.)

Each Registry key has a name, which is a string of up to 260 printable characters that can't include backslash characters (\) or wildcards (? and *). Names beginning with a period are reserved. Each key can contain subkeys and values. Under Windows 3.1, a key could hold only one unnamed value, while 32-bit platforms allow an unlimited number of values. (But unnamed values, called the default values, are maintained for backward compatibility.)

NOTE
In general, Windows 9x and Windows NT greatly differ in how they deal with the Registry. In Windows NT, you must account for additional security issues, and in general you have no guarantee that you can open an existing Registry key or value. In this section, I stayed clear of such details and focused on those functions that behave the same way for all the Windows platforms. For this reason, I've sometimes used "old" Registry functions instead of newer ones, which you recognize by the Ex suffix in their names, a suffix that stands for "Extended."

Working with keys

Navigating the Registry is similar to exploring a directory tree: To reach a given file, you must open the directory that contains it. Likewise, you reach a Registry subkey from another open key at a higher level in the Registry hierarchy. You must open a key before reading its subkeys and its values, and to do that you must supply the handle of another open key in the Registry. After you've worked with a key you must close it, as you do with files. The only keys that are always open and that don't need to be closed are the top-level keys listed in Table A-1. You open a key with the RegOpenKeyEx API function:

Declare Function RegOpenKeyEx Lib "advapi32.dll" Alias "RegOpenKeyExA" _
    (ByVal hKey As Long, ByVal lpSubKey As String, ByVal ulOptions As _
     Long, ByVal samDesired As Long, phkResult As Long) As Long

hKey is the handle of an open key and can be one of the values listed in Table A1 or the handle of a key that you've opened previously. lpSubKey is the path from the hKey key to the key that you want to open. ulOptions is a reserved argument and must be 0. samDesired is the type of access you want for the key that you want to open and is a symbolic constant such as KEY_READ, KEY_WRITE, or KEY_ALL_ACCESS. Finally, phkResult is a Long variable passed by reference, which receives the handle of the key opened by the function if the operation is successful. You can test the success of the open operation by looking at the return value of the RegOpenKeyEx function: A zero value means that the operation succeeded, and any non-zero value is an error code. This behavior is common to all the Registry API functions, so you can easily set up a function that tests the success state of any call. (See the MSDN documentation for the list of error codes.)

As I mentioned earlier, you must close any open key as soon as you don't need it any longer, which you do with the RegCloseKey API function. This function takes the handle of the key to be closed as its only argument, and returns 0 if the operation is successful:

Declare Function RegCloseKey Lib "advapi32.dll" (ByVal hKey As Long) _
    As Long

Frequently, the presence of a subkey is enough to store significant data in a key. For example, if the machine has a math coprocessor, Windows creates the following key:

HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System\FloatingPointProcessor

so you can test the presence of the coprocessor using this routine:

' Assumes that all symbolic constants are correctly declared elsewhere.
Function MathProcessor() As Boolean
    Dim hKey As Long, Key As String
    Key = "HARDWARE\DESCRIPTION\System\FloatingPointProcessor"
    If RegOpenKeyEx(HKEY_LOCAL_MACHINE, Key, 0, KEY_READ, hKey) = 0 Then
        ' If the open operation succeeded, the key exists.
        MathProcessor = True
        ' Important: close the key before exiting.
        RegCloseKey hKey
    End If
End Function

As you might expect, the Registry API includes a function for creating new keys, but its syntax is overly complex:

Declare Function RegCreateKeyEx Lib "advapi32.dll" Alias "RegCreateKeyExA"_
    (ByVal hKey As Long, ByVal lpSubKey As String, ByVal Reserved As Long,_
    ByVal lpClass As Long, ByVal dwOptions As Long, _
    ByVal samDesired As Long, ByVal lpSecurityAttributes As Long, _
    phkResult As Long, lpdwDisposition As Long) As Long

Most of the arguments have the same names and syntax as those that I've already described for the RegOpenKeyEx function, and I won't describe most of the new arguments because they constitute a topic too advanced for this context. You can pass a Long variable to the lpdwDisposition argument, and when the function returns you can test the contents in this variable. The value REG_CREATED_NEW_KEY (1) means that the key didn't exist and has been created and opened by this function, whereas the value REG_OPENED_EXISTING_KEY (2) means that the key already existed and the function just opened it without altering the Registry in any way. To reduce the confusion, I use the following routine, which creates a key if necessary and returns True if the key already existed:

Function CreateRegistryKey(ByVal hKey As Long, ByVal KeyName As String) _
    As Boolean
    Dim handle As Long, disp As Long
    If RegCreateKeyEx(hKey, KeyName, 0, 0, 0, 0, 0, handle, disp) Then
        Err.Raise 1001, , "Unable to create the registry key"
    Else
        ' Return True if the key already existed.
        If disp = REG_OPENED_EXISTING_KEY Then CreateRegistryKey = True
        ' Close the key.
        RegCloseKey handle
    End If
End Function

The following code snippet shows how you can use the CreateRegistryKey function to create a key with the name of your company under the key HKEY _CURRENT_USER\Software and another key with the name of your application. This is the approach followed by most commercial applications, including all those by Microsoft and other leading software companies:

CreateRegistryKey HKEY_CURRENT_USER, "Software\YourCompany"
CreateRegistryKey HKEY_CURRENT_USER, "Software\YourCompany\YourApplication"

NOTE
The CreateRegistryKey function, like all other Registry routines provided on the companion CD, always closes a key before exiting. This approach makes them "safe" but it also imposes a slight performance penalty because each call opens and closes a key that you might have to reopen immediately afterwards, as in the preceding example. You can't always have it all.

Finally, you can delete a key from the Registry, using the RegDeleteKey API function:

Declare Function RegDeleteKey Lib "advapi32.dll" Alias "RegDeleteKeyA" _
    (ByVal hKey As Long, ByVal lpSubKey As String) As Long

Under Windows 95 and 98, this function deletes a key and all its subkeys, whereas under Windows NT you get an error if the key being deleted contains other keys. For this reason, you should manually delete all the subkeys first:

' Delete the keys created in the previous example.
RegDeleteKey HKEY_CURRENT_USER, "Software\YourCompany\YourApplication"
RegDeleteKey HKEY_CURRENT_USER, "Software\YourCompany"

Working with values

In many cases, a Registry key contains one or more values, so you must learn how to read the information they contain. To do so, you need the RegQueryValueEx API function:

Declare Function RegQueryValueEx Lib "advapi32.dll" Alias _
    "RegQueryValueExA" (ByVal hKey As Long, ByVal lpValueName As String, _
    ByVal lpReserved As Long, lpType As Long, lpData As Any, _
    lpcbData As Long) As Long

hKey is the handle of the open key that contains the value. lpValueName is the name of the value you want to read. (Use an empty string for the default value.) lpReserved must be zero. lpType is the type of the key. lpData is a pointer to a buffer that will receive the data. lpcbData is a Long variable passed by reference; on entry it has to contain the size in bytes of the buffer, and on exit it contains the number of bytes actually stored in the buffer. Most Registry values you'll want to read are of type REG_DWORD (a Long value), REG_SZ (a null-terminated string), or REG_BINARY (array of bytes).

The Visual Basic environment stores some of its configuration settings as values under the following key:

HKEY_CURRENT_USER\Software\Microsoft\VBA\Microsoft Visual Basic

You can read the FontHeight value to retrieve the size of the font used for the code editor, whereas the FontFace value holds the name of the font. Because the former value is a Long number and the latter is a string, you need two different coding techniques for them. Reading a Long value is simpler because you just pass a Long variable by reference to lpData and pass its length in bytes (which is 4 bytes) in lpcbData. To retrieve a string value, on the other hand, you must prepare a buffer and pass it by value, and when the function returns you must strip off the characters in excess:

Dim KeyName As String, handle As Long
Dim FontHeight As Long, FontFace As String, FontFaceLen As Long

KeyName = "Software\Microsoft\VBA\Microsoft Visual Basic"
If RegOpenKeyEx(HKEY_CURRENT_USER, KeyName, 0, KEY_READ, handle) Then
    MsgBox "Unable to open the specified Registry key"
Else
    ' Read the "FontHeight" value.
    If RegQueryValueEx(handle, "FontHeight", 0, REG_DWORD, FontHeight, 4) _
        = 0 Then
        Print "Face Height = " & FontHeight
    End If

    ' Read the "FontFace" value. 
    FontFaceLen = 128                   ' Prepare the receiving buffer.
    FontFace = Space$(FontFaceLen)
    ' Notice that FontFace is passed using ByVal.
    If RegQueryValueEx(handle, "FontFace", 0, REG_SZ, ByVal FontFace, _
        FontFaceLen) = 0 Then
        ' Trim excess characters, including the trailing Nullchar.
        FontFace = Left$(FontFace, FontFaceLen - 1)
        Print "Face Name = " & FontFace
    End If
    ' Close the Registry key.
    RegCloseKey handle
End If

Because you need to read Registry values often, I've prepared a reusable function that performs all the necessary operations and returns the value in a Variant. You can also specify a default value, which you can use if the specified key or value doesn't exist. This tactic is similar to what you do with the Visual Basic's intrinsic GetSetting function.

Function GetRegistryValue(ByVal hKey As Long, ByVal KeyName As String, _
    ByVal ValueName As String, ByVal KeyType As Integer, _
    Optional DefaultValue As Variant = Empty) As Variant

    Dim handle As Long, resLong As Long
    Dim resString As String, length As Long
    Dim resBinary() As Byte
    ' Prepare the default result.
    GetRegistryValue = DefaultValue
    ' Open the key, exit if not found.
    If RegOpenKeyEx(hKey, KeyName, 0, KEY_READ, handle) Then Exit Function

    Select Case KeyType
        Case REG_DWORD
            ' Read the value, use the default if not found.
            If RegQueryValueEx(handle, ValueName, 0, REG_DWORD, _
                resLong, 4) = 0 Then
                GetRegistryValue = resLong
            End If
        Case REG_SZ
            length = 1024: resString = Space$(length)
            If RegQueryValueEx(handle, ValueName, 0, REG_SZ, _
                ByVal resString, length) = 0 Then
                ' If value is found, trim characters in excess.
                GetRegistryValue = Left$(resString, length - 1)
            End If
        Case REG_BINARY
            length = 4096
            ReDim resBinary(length - 1) As Byte
            If RegQueryValueEx(handle, ValueName, 0, REG_BINARY, _
                resBinary(0), length) = 0 Then ReDim Preserve resBinary(length-1)As Byte
                GetRegistryValue = resBinary()
            End If
        Case Else
            Err.Raise 1001, , "Unsupported value type"
    End Select
    RegCloseKey handle
End Function

To create a new Registry value or to modify the data of an existing value, you use the RegSetValueEx API function:

Declare Function RegSetValueEx Lib "advapi32.dll" Alias "RegSetValueExA" _
    (ByVal hKey As Long, ByVal lpValueName As String, _
    ByVal Reserved As Long, ByVal dwType As Long, lpData As Any, _
    ByVal cbData As Long) As Long

Let's see how we can add a LastLogin value under the key HKEY _CURRENT_USER\Software\YourCompany\YourApplication, that we created in the previous section:

Dim handle As Long, strValue As String
' Open the key, check if any error occurred.
If RegOpenKeyEx(HKEY_CURRENT_USER, "Software\YourCompany\YourApplication",_
    0, KEY_WRITE, handle) Then 
    MsgBox "Unable to open the key."
Else
    ' We want to add a "LastLogin" value, of type string.
    strValue = FormatDateTime(Now)
    ' Strings must be passed using ByVal.
    RegSetValueEx handle, "LastLogin", 0, REG_SZ, ByVal strValue, _
        Len(strValue)
    ' Don't forget to close the key.
    RegCloseKey handle
End If

On the companion CD, you'll find the source code of the SetRegistryValue function, which automatically uses the correct syntax according to the type of value you're creating. Finally, by using the RegDeleteValue API function, you can delete a value under a key that you opened previously:

Declare Function RegDeleteValue Lib "advapi32.dll" Alias "RegDeleteValueA"_
    (ByVal hKey As Long, ByVal lpValueName As String) As Long

Enumerating keys and values

When you're exploring the Registry, you often need to enumerate all the keys or all the values under a key. The function you use to enumerate keys is RegEnumKey:

Private Declare Function RegEnumKey Lib "advapi32.dll" _
    Alias "RegEnumKeyA" (ByVal hKey As Long, ByVal dwIndex As Long, _
    ByVal lpName As String, ByVal cbName As Long) As Long

You must pass the handle of an open Registry key in the hKey argument, and then you repeatedly call this function, passing increasing index values in dwIndex. The lpName argument must be a string buffer of at least 260 characters (the maximum length for a key name), and lpcbName is the length of the buffer. When you exit the routine, the buffer contains a Null-terminated string, so you have to strip all the characters in excess. To simplify your job, I've prepared a function that iterates on all the subkeys of a given keys and returns an String array that contains the names of all the subkeys:

Function EnumRegistryKeys(ByVal hKey As Long, ByVal KeyName As String) _
    As String()
    Dim handle As Long, index As Long, length As Long
    ReDim result(0 To 100) As String

    ' Open the key, exit if not found.
    If Len(Keyname) Then
        If RegOpenKeyEx(hKey, KeyName, 0, KEY_READ, handle) Then 
            Exit Function
        End If
        ' Subsequent functions use hKey.
        hKey = handle
    End If
    
    For index = 0 To 999999
        ' Make room in the array.
        If index > UBound(result) Then
            ReDim Preserve result(index + 99) As String
        End If
        length = 260                   ' Max length for a key name.
        result(index) = Space$(length)
        If RegEnumKey(hKey, index, result(index), length) Then Exit For
        ' Trim characters in excess.
        result(index) = Left$(result(index), InStr(result(index), _
            vbNullChar) - 1)
    Next

    ' Close the key, if it was actually opened.
    If handle Then RegCloseKey handle
    ' Trim unused items in the array and return the results to the caller.
    ReDim Preserve result(index - 1) As String
    EnumRegistryKeys = result()
End Function

Thanks to the EnumRegistryKey function, it's simple to dig a lot of useful information out of the Registry. For example, see how easy it is to fill a ListBox control with the names of all the components registered on the machine under the HKEY_CLASS_ROOT key:

Dim keys() As String, i As Long
keys() = EnumRegistryKeys(HKEY_CLASSES_ROOT, "")
List1.Clear
For i = LBound(keys) To UBound(keys)
    List1.AddItem keys(i)
Next

The companion CD includes a demonstration program (shown in Figure A-7) that displays the list of installed COM components as well as their CLSIDs and the DLL or EXE file that contains each one of them. You can easily expand this first version to create your own utilities that track anomalies in the Registry. For example, you can list all the DLL and EXE files that aren't in the locations listed in the Registry. (COM raises an error when you try to instantiate such components.)

Click to view at full size.

Figure A-7. You can use Registry API routines to list all the components installed on your machine, with their CLSIDs and the locations of their executable files.

The Windows API also exposes a function for enumerating all the values under a given open key:

Declare Function RegEnumValue Lib "advapi32.dll" Alias "RegEnumValueA" _
    (ByVal hKey As Long, ByVal dwIndex As Long, ByVal lpValueName As _
    String, lpcbValueName As Long, ByVal lpReserved As Long, _
    lpType As Long, lpData As Any, lpcbData As Long) As Long

This function returns the type of each value in the lpType variable and the contents of the value in lpData. The difficulty is that you don't know in advance what the type of the value is, and therefore you don't know the kind of variable—Long, String, or Byte array—you should pass in lpData. The solution to this problem is to pass a Byte array and then move the result into a Long variable using the CopyMemory API routine or into a String variable using the VBA StrConv function. On the companion CD, you'll find the complete source of the EnumRegistryValues routine, which encapsulates all these details and returns a two-dimensional array of Variants containing all the values' names and data. For example, you can use this routine to retrieve all the Microsoft Visual Basic configuration values:

Dim values() As Variant, i As Long
values() = EnumRegistryValues(HKEY_CURRENT_USER, _
    "Software\Microsoft\VBA\Microsoft Visual Basic")
For i = LBound(values, 2) To UBound(values, 2)
    ' Row 0 holds the value's name, row 1 holds its value.
    List1.AddItem values(0, i) & " = " & values(1, i)
Next